﻿// HSS.Data.ModelGenerator.Processor.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license
// http://www.opensource.org/licenses/mit-license.html
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2010
// ----------------------------------------------------------------------------
// File:       Processor.cs
// Author:     HSS\gbanta
// Created:    08/12/2010
// Modified:   12/04/2010
// ----------------------------------------------------------------------------
namespace HSS.Data.ModelGenerator
{
	#region Using Directives
	using System;
	using System.Collections.Generic;
	using System.Data;
	using System.Globalization;
	using System.Linq;
	using System.Text;
	using System.Data.SqlClient;

	#endregion

	#region Processor
	/// <summary>
	/// Provides the methods for generating a C# Entity
	/// from an MS SQL Schema Set.
	/// </summary>
	public sealed class Processor
	{
		#region Fields
		string ctorDetail;
		string ctorPrameters;
		string properties;
		string loaderDetail;
		IEnumerable<DataRow> objectProperties;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="objectDefinition">The <see cref="ObjectDefinition"/> to be processed.</param>
		public Processor(ObjectDefinition objectDefinition)
		{
			Guard.ArgumentNotNull(objectDefinition, "objectDefinition");
			this.ObjectDefinition = objectDefinition;
		}
		/// <summary>
		/// Immediately releases all resources owned by the object.
		/// </summary>
		public void Dispose()
		{
			this.Dispose(true);
			GC.SuppressFinalize(this);
		}
		/// <summary>
		/// Immediately releases all resources owned by the object.
		/// </summary>
		/// <param name="disposed"></param>
		private void Dispose(bool disposed)
		{
		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets the class name from the <see cref="ObjectDefinition"/>.
		/// </summary>
		public string ClassName { get { return this.ObjectDefinition.ClassName; } }
		/// <summary>
		/// Gets or sets the name of the person creating this object model.
		/// </summary>
		public string Creator { get; set; }
		/// <summary>
		/// Gets or sets the Namespace this Entity will be contained within.
		/// </summary>
		public string NamespaceName { get; set; }
		/// <summary>
		/// Gets or sets the Base Class this Entity will inherit from.
		/// </summary>
		public string BaseClass { get; set; }
		/// <summary>
		/// Gets the current objects schema name.
		/// </summary>
		public string Schema { get { return this.ObjectDefinition.Schema; } }
		/// <summary>
		/// Gets the db object name from the <see cref="ObjectDefinition"/>.
		/// </summary>
		public string ObjectName { get { return this.ObjectDefinition.ObjectName; } }
		/// <summary>
		/// Gets the underlying <see cref="ObjectDefinition"/>.
		/// </summary>
		public ObjectDefinition ObjectDefinition
		{
			get;
			private set;
		}
		/// <summary>
		/// Getst he generated <see cref="System.Data.DataTable"/> containing the schema.
		/// </summary>
		public DataTable SchemaTable { get; private set; }
		/// <summary>
		/// Gets the last error if any.
		/// </summary>
		public Exception LastError { get; set; }
		/// <summary>
		/// Gets or sets the Template used to generate the Entity from.
		/// </summary>
		public string CurrentTemplate
		{
			get
			{
				if (string.IsNullOrEmpty(_currentTemplate))
				{
					if (this.UsePartialClass)
						_currentTemplate = Templates.DefaultPartialEntityFileTemplate;
					else
						_currentTemplate = Templates.DefaultSingleFileTemplate;
				}
				return _currentTemplate;
			}
			set
			{
				_currentTemplate = value;
			}
		} string _currentTemplate;
		/// <summary>
		/// Gets or sets the Template used to generate the partial class 
		/// containing the IDataEntity implemention of the Entity. 
		/// This is only used if <see cref="UsePartialClass"/> is set 
		/// to true.
		/// </summary>
		public string CurrentEntityTemplate
		{
			get
			{
				if (string.IsNullOrEmpty(_currentEntityTemplate))
					_currentEntityTemplate = Templates.DefaultPartialContextFileTemplate;
				return _currentEntityTemplate;
			}
			set
			{
				_currentEntityTemplate = value;
			}
		} string _currentEntityTemplate;
		/// <summary>
		/// Gets or sets whether or not to include the DataMemberAttribute attribute.
		/// </summary>
		public bool UseDataMemberAttribute { get; set; }
		/// <summary>
		/// Gets or sets whether or not to generate a partial class for the 
		/// IDataEntity implementation.
		/// </summary>
		public bool UsePartialClass { get; set; }
		/// <summary>
		/// Gets or sets whether or not to implement the Property accessors.
		/// </summary>
		public bool ImplementPropertyAccessors { get; set; }
		/// <summary>
		/// Gets or sets to include a call to OnPropertyChanging if <see cref="ImplementPropertyAccessors"/> is also true.
		/// </summary>
		public bool CallOnPropertyChanging { get; set; }
		/// <summary>
		/// Gets or sets to include a call to OnPropertyChanged if <see cref="ImplementPropertyAccessors"/> is also true.
		/// </summary>
		public bool CallOnPropertyChanged { get; set; }
		/// <summary>
		/// Gets or sets to include a call to ValidateProperty if <see cref="ImplementPropertyAccessors"/> is also true.
		/// </summary>
		public bool CallValidateProperty { get; set; }
		/// <summary>
		/// Gets or sets to check if the value is different before a call to OnPropertyChanged if <see cref="CallOnPropertyChanged"/> and <see cref="ImplementPropertyAccessors"/> are also true.
		/// </summary>
		public bool ImplementCheckNewValue { get; set; }
		/// <summary>
		/// Gets or sets if the Load methods populates the private accessor or the public property.
		/// </summary>
		public bool UsePrivateAccessorForLoad { get; set; }
		/// <summary>
		/// Gets the resultant Entity code.
		/// </summary>
		public string ClassEntityCode { get; private set; }
		/// <summary>
		/// Gets the resultant Context partial class code
		/// if the <see cref="UsePartialClass"/> is true.
		/// </summary>
		public string ClassContextCode { get; private set; }
		/// <summary>
		/// Gets or sets if the schema definition for a field is readonly, make the property setter protected.
		/// </summary>
		public bool SetterProtectedIfReadOnly { get; set; }
		/// <summary>
		/// Gets or sets the ApplyHSSDBAttributes value.
		/// </summary>
		public bool ApplyHSSDBAttributes { get; set; }
		#endregion

		#region Methods

		/// <summary>
		/// Begins the object model generation process.
		/// </summary>
		public void Process()
		{
			if (this.GetSchema())
			{
				this.SetPropertyList();
				this.GenerateCode();
				this.ProcessTemplate();
				return;
			}

			this.ClassEntityCode = string.Empty;
			this.ClassContextCode = string.Empty;
		}

		private bool GetSchema()
		{
			if (this.ObjectDefinition.IsTable)
				return this.GetTableOrViewSchema();
			else
				return this.GetStoredProcedureSchema();
		}
		private bool GetStoredProcedureSchema()
		{
			if (null == this.SchemaTable)
			{
				using (var da = new DataAccess(DataAccessContext.DefaultConnection))
				{
					try
					{
						foreach (var p in this.ObjectDefinition.Parameters)
						{
							var val = GetParameterValue(p.DbType.GetType(), p.DbType.ToString());
							if (val == null)
								val = GetParameterDefaultValue(p.DbType);
							da.AddWithValue(p.ParameterName, val);
						}

						using (var sdr = da.ExecSqlDataReader(this.Schema + "." + this.ObjectName, CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo))
							this.SchemaTable = sdr.GetSchemaTable();

						return null != this.SchemaTable;
					}
					catch (Exception e)
					{
						this.LastError = e;
					}
				}

				return false;
			}
			else
				return true;
		}
		private bool GetTableOrViewSchema()
		{
			if (null == this.SchemaTable)
			{
				using (var sqlConn = new SqlConnection(DataAccessContext.GetDefaultConnectionString()))
				{
					try
					{
						sqlConn.Open();
						using (var sqlCmd = new SqlCommand(this.ObjectDefinition.SqlDefinition, sqlConn))
						{
							using (var sqlDataReader = sqlCmd.ExecuteReader(CommandBehavior.KeyInfo))
								this.SchemaTable = sqlDataReader.GetSchemaTable();
							return true;
						}
					}
					catch (Exception e)
					{
						this.LastError = e;
					}
				}

				return false;
			}
			else
				return true;
		}
		private void GenerateCode()
		{
			string parameters = string.Empty;
			string ctor = string.Empty;
			string loader = string.Empty;
			StringBuilder propertyBuilder = new StringBuilder();

			try
			{
				foreach (DataRow dataRow in this.objectProperties)
				{
					int length;
					string propertyName, dataType, dataTypeName;
					bool allowDBNull, isHidden, isReadOnly, isKey, isExpression;
					if (!GetPropertyInfo(dataRow, out propertyName, out dataType, out dataTypeName, out length, out allowDBNull, out isHidden, out isReadOnly, out isKey, out isExpression))
						continue;

					if (isHidden)
						continue;

					// Assemble the text
					string fieldName = propertyName[0].ToString().ToLower(CultureInfo.CurrentUICulture) + propertyName.Substring(1);
					string privateAccessor = "_" + propertyName.Substring(0, 1).ToLower() + propertyName.Substring(1);

					// Parameters
					parameters += dataType + " " + fieldName + ", ";

					// Constructor
					if (this.ImplementPropertyAccessors && UsePrivateAccessorForLoad)
						ctor += "			this." + privateAccessor + " = " + fieldName + ";" + Environment.NewLine;
					else
						ctor += "			this." + propertyName + " = " + fieldName + ";" + Environment.NewLine;

					#region Loader

					string dbNullSpace = "	";
					if (allowDBNull)
					{
						dbNullSpace = "		";
						loader += "			if (!dr.IsDBNull(dr.GetOrdinal(\"" + propertyName + "\")))" + Environment.NewLine;
					}

					var loadAccessor = dbNullSpace + "		this." + propertyName;
					if (this.ImplementPropertyAccessors && UsePrivateAccessorForLoad)
						loadAccessor = dbNullSpace + "		this." + privateAccessor;

					if (dataType == "Object")
						loader += loadAccessor + " = dr.GetValue(dr.GetOrdinal(\"" + propertyName + "\"));" + Environment.NewLine;
					else if (dataType == "Single")
						loader += loadAccessor + " = dr.GetFloat(dr.GetOrdinal(\"" + propertyName + "\"));" + Environment.NewLine;
					else if (dataType == "Byte[]")
						loader += loadAccessor + " = (byte[])dr.GetValue(dr.GetOrdinal(\"" + propertyName + "\"));" + Environment.NewLine;
					else
						loader += loadAccessor + " = dr.Get" + dataType + "(dr.GetOrdinal(\"" + propertyName + "\"));" + Environment.NewLine;

					#endregion

					#region Property Builder

					#region Preamble

					propertyBuilder.AppendFormat(@"		/// <summary>{0}", Environment.NewLine);
					if (isReadOnly && SetterProtectedIfReadOnly)
						propertyBuilder.AppendFormat(@"		/// Gets the {0} value.{1}", propertyName, Environment.NewLine);
					else
						propertyBuilder.AppendFormat(@"		/// Gets or sets the {0} value.{1}", propertyName, Environment.NewLine);
					propertyBuilder.AppendFormat(@"		/// </summary>{0}", Environment.NewLine);
					if (this.UseDataMemberAttribute)
						propertyBuilder.AppendFormat(@"		[DataMember]{0}", Environment.NewLine);

					if (this.ApplyHSSDBAttributes)
					{
						if (isKey)
						{
							if (IsValidPrimaryKeyType(dataType))
								propertyBuilder.AppendFormat(@"		[PrimaryKey]{0}", Environment.NewLine);
							else
								propertyBuilder.AppendFormat(@"		[DoNotStore]{0}", Environment.NewLine);
						}
						else if (!IsSupportedPropertyType(dataType))
							propertyBuilder.AppendFormat(@"		[DoNotStore]{0}", Environment.NewLine);
						else if (dataType == "String")
						{
							if (length == -1)
								propertyBuilder.AppendFormat(@"		[CharLength(2048)]{0}", Environment.NewLine);
							else
								propertyBuilder.AppendFormat(@"		[CharLength(" + length + ")]{0}", Environment.NewLine);
						}
					}

					#endregion

					if (!this.ImplementPropertyAccessors)
					{
						#region Dynamic Backing Fields
						if (isReadOnly && SetterProtectedIfReadOnly)
						{
							if (allowDBNull && dataType != "String")
								propertyBuilder.AppendFormat(@"		public {0}? {1} {{ get; protected set; }}{2}", dataType, propertyName, Environment.NewLine);
							else
								propertyBuilder.AppendFormat(@"		public {0} {1} {{ get; protected set; }}{2}", dataType, propertyName, Environment.NewLine);
						}
						else
						{
							if (allowDBNull && dataType != "String")
								propertyBuilder.AppendFormat(@"		public {0}? {1} {{ get; set; }}{2}", dataType, propertyName, Environment.NewLine);
							else
								propertyBuilder.AppendFormat(@"		public {0} {1} {{ get; set; }}{2}", dataType, propertyName, Environment.NewLine);
						}
						#endregion
					}
					else
					{
						#region Property Declaration
						if (allowDBNull && dataType != "String")
							propertyBuilder.AppendFormat(@"		public {0}? {1}{2}", dataType, propertyName, Environment.NewLine);
						else
							propertyBuilder.AppendFormat(@"		public {0} {1}{2}", dataType, propertyName, Environment.NewLine);
						#endregion

						#region Property implementation

						propertyBuilder.AppendFormat(@"		{{{0}", Environment.NewLine);
						propertyBuilder.AppendFormat(@"			get {{ return {0}; }}{1}", privateAccessor, Environment.NewLine);

						if (!CallOnPropertyChanging && !CallOnPropertyChanged && !CallValidateProperty && !ImplementCheckNewValue)
						{
							propertyBuilder.AppendFormat("			" + ((isReadOnly && SetterProtectedIfReadOnly) ? "protected " : "") + "set {{ {0} = value; }}{1}", privateAccessor, Environment.NewLine);
						}
						else
						{
							propertyBuilder.AppendFormat("			" + ((isReadOnly && SetterProtectedIfReadOnly) ? "protected " : "") + "set{0}", Environment.NewLine);
							propertyBuilder.AppendFormat("			{{{0}", Environment.NewLine);
							if (ImplementCheckNewValue)
							{
								propertyBuilder.AppendFormat("				if ({0} != value){1}", privateAccessor, Environment.NewLine);
								propertyBuilder.AppendFormat("				{{{0}", Environment.NewLine);
								if (CallOnPropertyChanging)
									propertyBuilder.AppendFormat("					this.OnPropertyChanging(\"{0}\");{1}", propertyName, Environment.NewLine);
								if (CallValidateProperty)
									propertyBuilder.AppendFormat("					this.ValidateProperty(\"{0}\", value);{1}", propertyName, Environment.NewLine);
								propertyBuilder.AppendFormat("					{0} = value;{1}", privateAccessor, Environment.NewLine);
								if (CallOnPropertyChanged)
									propertyBuilder.AppendFormat("					this.OnPropertyChanged(\"{0}\");{1}", propertyName, Environment.NewLine);
								propertyBuilder.AppendFormat("				}}{0}", Environment.NewLine);
							}
							else
							{
								if (CallOnPropertyChanging)
									propertyBuilder.AppendFormat("				this.OnPropertyChanging(\"{0}\");{1}", propertyName, Environment.NewLine);
								if (CallValidateProperty)
									propertyBuilder.AppendFormat("				this.ValidateProperty(\"{0}\", value);{1}", propertyName, Environment.NewLine);
								propertyBuilder.AppendFormat("				{0} = value;{1}", privateAccessor, Environment.NewLine);
								if (CallOnPropertyChanged)
									propertyBuilder.AppendFormat("				this.OnPropertyChanged(\"{0}\");{1}", propertyName, Environment.NewLine);
							}
							propertyBuilder.AppendFormat("			}}{0}", Environment.NewLine);
						}

						#endregion

						#region Property Backing Field
						if (allowDBNull && dataType != "String")
							propertyBuilder.AppendFormat("		}} {0}? {1};{2}", dataType, privateAccessor, Environment.NewLine);
						else
							propertyBuilder.AppendFormat("		}} {0} {1};{2}", dataType, privateAccessor, Environment.NewLine);
						#endregion

					}
					#endregion
				}

				// Remove leading tabs and strip the final Environment.NewLine.
				this.ctorDetail = ctor.Trim();

				// Populate the loader
				this.loaderDetail = loader.Trim();

				// Strip the final comma.
				if (parameters.Length > 0)
				{
					int intIndex = parameters.LastIndexOf(",", StringComparison.OrdinalIgnoreCase);
					if (intIndex >= 0)
						this.ctorPrameters = parameters.Remove(intIndex);
				}

				// Properties
				this.properties = propertyBuilder.ToString().Trim();

			}
			catch (Exception e)
			{
				this.LastError = e;
			}
		}
		private string GetPrimaryKey()
		{
			string pk = Properties.Resources.NO_PRIMARYKEY;

			try
			{
				if (null != this.SchemaTable && this.SchemaTable.Rows.Count > 0)
				{
					foreach (DataRow row in this.SchemaTable.Rows)
					{
						if (row.Field<bool>("IsKey") && !row.Field<bool>("IsHidden"))
							return row.Field<string>("ColumnName");
					}
				}
			}
			catch { }

			return pk;
		}
		private void ProcessTemplate()
		{
			if (UsePartialClass)
			{
				this.ClassEntityCode =
					this.CurrentTemplate
						.Replace(Template.AUTHOR, this.Creator)
						.Replace(Template.CTORBODY, this.ctorDetail)
						.Replace(Template.CTORPARAMETERS, this.ctorPrameters)
						.Replace(Template.DATECREATED, DateTime.Today.ToShortDateString())
						.Replace(Template.NAMESPACE, this.NamespaceName)
						.Replace(Template.BASECLASS, this.BaseClass)
						.Replace(Template.OBJECTNAME, this.ClassName)
						.Replace(Template.PRIMARYKEY, this.GetPrimaryKey())
						.Replace(Template.PROPERTIES, this.properties)
						.Replace(Template.LOADER, this.loaderDetail);
				this.ClassContextCode =
					this.CurrentEntityTemplate
						.Replace(Template.AUTHOR, this.Creator)
						.Replace(Template.CTORBODY, this.ctorDetail)
						.Replace(Template.CTORPARAMETERS, this.ctorPrameters)
						.Replace(Template.DATECREATED, DateTime.Today.ToShortDateString())
						.Replace(Template.NAMESPACE, this.NamespaceName)
						.Replace(Template.OBJECTNAME, this.ClassName)
						.Replace(Template.PRIMARYKEY, this.GetPrimaryKey())
						.Replace(Template.PROPERTIES, this.properties)
						.Replace(Template.LOADER, this.loaderDetail);
			}
			else
			{
				this.ClassEntityCode =
					this.CurrentTemplate
						.Replace(Template.AUTHOR, this.Creator)
						.Replace(Template.CTORBODY, this.ctorDetail)
						.Replace(Template.CTORPARAMETERS, this.ctorPrameters)
						.Replace(Template.DATECREATED, DateTime.Today.ToShortDateString())
						.Replace(Template.NAMESPACE, this.NamespaceName)
						.Replace(Template.OBJECTNAME, this.ClassName)
						.Replace(Template.BASECLASS, this.BaseClass)
						.Replace(Template.PRIMARYKEY, this.GetPrimaryKey())
						.Replace(Template.PROPERTIES, this.properties)
						.Replace(Template.LOADER, this.loaderDetail);
			}
		}
		private void SetPropertyList()
		{
			this.objectProperties = this.SchemaTable.AsEnumerable().Distinct(new RowComparer());
		}
		private bool GetPropertyInfo(DataRow row, out string propertyName, out string dataType, out string dataTypeName, out int length, out bool allowDBNull, out bool isHidden, out bool isReadOnly, out bool isKey, out bool isExpression)
		{
			propertyName = dataType = dataTypeName = string.Empty;
			allowDBNull = isHidden = isReadOnly = isKey = isExpression = false;
			length = 0;

			propertyName = row[0].ToString();
			if (string.IsNullOrEmpty(propertyName))
				return false;

			if (propertyName == "timestamp")
				return false;

			dataType = row["DataType"].ToString().Replace("System.", string.Empty);
			if (string.IsNullOrEmpty(dataType))
				return false;
			dataTypeName = row["DataTypeName"].ToString().ToLowerInvariant();
			length = (int)row["ColumnSize"];

			isExpression = (bool)row["IsExpression"];
			allowDBNull = (bool)row["AllowDBNull"];
			isHidden = (bool)row["IsHidden"];
			isReadOnly = (bool)row["IsReadOnly"];
			isKey = (bool)row["IsKey"];
			if (isKey || isExpression)
				isReadOnly = true;

			return true;
		}

		private static bool IsSupportedPropertyType(string type)
		{
			return

				type == typeof(Boolean).Name ||
				type == typeof(Byte).Name ||
				type == typeof(Char).Name ||
				type == typeof(DateTime).Name ||
				type == typeof(DateTimeOffset).Name ||
				type == typeof(TimeSpan).Name ||
				type == typeof(Decimal).Name ||
				type == typeof(Double).Name ||
				type == typeof(Int16).Name ||
				type == typeof(Int32).Name ||
				type == typeof(Int64).Name ||
				type == typeof(SByte).Name ||
				type == typeof(Single).Name ||
				type == typeof(String).Name ||
				type == typeof(UInt16).Name ||
				type == typeof(UInt32).Name ||
				type == typeof(UInt64).Name ||
				type == typeof(Guid).Name;
		}
		private static bool IsValidPrimaryKeyType(string type)
		{
			return
				type == typeof(System.Int32).Name ||
				type == typeof(System.Int64).Name ||
				type == typeof(System.Guid).Name;
		}
		private static object GetParameterValue(Type type, string dbType)
		{
			return type.Assembly.CreateInstance(dbType);
		}
		private static object GetParameterDefaultValue(DbType dbType)
		{
			object val = null;
			switch (dbType)
			{
				case DbType.Binary:
					val = new byte[] { };
					break;

				case DbType.AnsiString:
				case DbType.AnsiStringFixedLength:
				case DbType.String:
				case DbType.StringFixedLength:
				case DbType.Xml:
					val = string.Empty;
					break;

				case DbType.Boolean:
					val = false;
					break;

				case DbType.Date:
				case DbType.DateTime:
				case DbType.DateTime2:
				case DbType.DateTimeOffset:
					val = DateTime.Now;
					break;

				case DbType.VarNumeric:
				case DbType.Decimal:
					val = (decimal)0;
					break;

				case DbType.Currency:
				case DbType.Double:
					val = (double)0;
					break;

				case DbType.Int64:
					val = (long)0;
					break;

				case DbType.Single:
					val = (float)0;
					break;

				case DbType.SByte:
					val = (byte)0;
					break;

				case DbType.Int16:
					val = (Int16)0;
					break;

				case DbType.Int32:
					val = (Int32)0;
					break;

				case DbType.UInt16:
					val = (UInt16)0;
					break;

				case DbType.UInt32:
					val = (UInt32)0;
					break;

				case DbType.UInt64:
					val = (UInt64)0;
					break;

				case DbType.Guid:
					val = Guid.Empty;
					break;

				case DbType.Object:
					val = new object();
					break;
			}

			return val;
		}
		#endregion

		#region RowComparer
		class RowComparer : IEqualityComparer<DataRow>
		{
			public RowComparer()
			{

			}

			#region IEqualityComparer<DataRow> Members

			public bool Equals(DataRow x, DataRow y)
			{
				if (null == x)
					return false;
				if (null == y)
					return false;
				return x[0].ToString() == y[0].ToString();
			}

			public int GetHashCode(DataRow obj)
			{
				if (null == obj)
					return 0;
				return obj[0].GetHashCode();
			}

			#endregion
		}
		#endregion
	}
	#endregion
}